/**
 * Copyright (c) 2012 Nokia Corporation. 
 */

package com.nokia.example.tictactoe.connectivity;

import java.io.IOException;
import java.util.Timer;
import java.util.TimerTask;

import javax.microedition.io.Connector;
import javax.microedition.io.DatagramConnection;
import javax.microedition.io.ServerSocketConnection;

/**
 * A UDP protocol based client for automatic peer discovery. 
 */
public class DiscoveryClient extends UdpClient {
	// Constants
	private static final String DEFAULT_IDENTIFIER = "DISCOVERY";
	private static final int BROADCAST_INTERVAL = 700; // Milliseconds 
	private static final int DEFAULT_IP_ADDRESS_RANGE = 30;
	
	// Members
	private Timer _timer = null;
	private String _addressStub = null;
	private int _timeout = 0;
	private int _timeoutCounter = 0;
	private static int _ipAddressRange = DEFAULT_IP_ADDRESS_RANGE;
	private int _firstIp = 0;
	private int _myIpEnding = 0;
	private int _ipEnding = 0;
	private static byte[] _broadcastMessage = null;
	private static String _localAddress = new String("localhost");
	private static String _appIdentifier = DEFAULT_IDENTIFIER;
	
	/**
	 * Constructor.
	 */
	public DiscoveryClient() {
		super();
		
		_port =  DiscoveryServer.DISCOVERY_PORT;
		
		// Create the datagram to broadcast, the format of the broadcasted
		// message is "TICTAC <IP address>:<port>".
		_localAddress = resolveLocalAddress();
		
		if (_localAddress == null) {
			_localAddress = new String("localhost");
		}
		
		_broadcastMessage = constructBroadcastMessage(_localAddress);
	}
	
	/**
	 * @param range The desired IP range for discovery.
	 */
	public static void setIpAddressRange(int range) {
		if (range > 1) {
			_ipAddressRange = range;
		}
	}
	
	/**
	 * Sets the app identifier and reconstructs the message to broadcast.
	 * 
	 * @param identifier The identifier that is broadcasted. This is the way
	 * 					 the devices know that they are looking for each other.
	 */
	public static void setAppIdentifier(String identifier) {
		if (identifier != null && identifier.length() > 0) {
			_appIdentifier = identifier;
			_broadcastMessage = constructBroadcastMessage(_localAddress);
		}
	}
	
	/**
	 * From Connection.
	 */
	public synchronized boolean open(String hostname, Integer port) {
		if (_running) {
			System.out.println("UdpClient::open(): Already open!");
			return false;
		}
		
		setState(CONNECTING);

		new Thread() {
			public void run() {
				_running = true;
				
				log("Discovery client started; " + _broadcastMessage);
				
				// Setup the broadcast timer
				_timer = new Timer();
				BroadcastTimerTask broadcastTimerTask = new BroadcastTimerTask();
				_timer.scheduleAtFixedRate(broadcastTimerTask,
										   BROADCAST_INTERVAL,
										   BROADCAST_INTERVAL);
				_timeoutCounter = 0;
				
				setState(CONNECTED);
				log("Broadcasting initiated");
				
				while (_running) {
					// Nothing to do but block the executing until cancel() is
					// called
				}
				
				setState(DISCONNECTING);
				
				if (_timer != null) {
					_timer.cancel();
					_timer = null;
				}
				
				try {
					if (_udpConnection != null) {
						_udpConnection.close();
					}
				}
				catch (IOException e) {}
				
				_udpConnection = null;
				setState(NOT_CONNECTED);
			}
		}.start();
		
		return true;
	}
	
	/** 
	 * @return The local address. If not resolved yet, "localhost" is returned.
	 */
	static public String localAddress() {
		return _localAddress;
	}

	/**
	 * Sets the timeout.
	 * @param milliseconds
	 */
	public void setTimeout(int milliseconds) {
		if (milliseconds >= 0) {
			_timeout = milliseconds;
		}
	}
	
	/**
	 * Resolves the local IP address using ServerSocketConnection.
	 * @return The local IP address or null in case of a failure.
	 */
	private String resolveLocalAddress() {
		String address = null;
		
		try {
			ServerSocketConnection ssc =
					(ServerSocketConnection)Connector.open("socket://:1234");
			address = ssc.getLocalAddress();
			ssc.close();
		}
		catch (IOException e) {
			notifyError(e.toString());
		}
		
		if (address != null && address.length() > 0) {			
			try {
				int index = address.lastIndexOf('.');
				_addressStub = address.substring(0, index + 1);
				_myIpEnding = Integer.parseInt(
						address.substring(index + 1, address.length()));
				System.out.println("DiscoveryClient::resolveLocalAddress(): _addressStub == "
						+ _addressStub + ", ending == " + _myIpEnding);
			}
			catch (Exception e) {}
			
			_firstIp = _myIpEnding > 0 ? _myIpEnding - 1 : _myIpEnding + 1;
			_ipEnding = _firstIp;
		}
		
		log("Local IP is " + address);
		return address;
	}
	
	/**
	 * Constructs a broadcast message based on the given host address.
	 * @param hostAddress The host address of the receiver.
	 * @return The constructed message as a byte array.
	 */
	private static byte[] constructBroadcastMessage(final String hostAddress) {
		StringBuffer buffer = new StringBuffer();
		buffer.append(_appIdentifier);
		buffer.append(" ");
		buffer.append(hostAddress);
		buffer.append(":");
		buffer.append(ConnectionManager.SERVER_PORT.toString());
		return buffer.toString().getBytes();			
	}
	
	/**
	 * Resolves the next IP to send the message to. 
	 * @return The next IP to send the message to.
	 */
	private String nextIp() {
		StringBuffer buffer = new StringBuffer();
		buffer.append(_addressStub);
		buffer.append(_ipEnding);
		int temp = 0;
		
		if (_ipEnding > _myIpEnding) {
			temp = _myIpEnding - (_ipEnding - _myIpEnding) - 1;
			
			if (temp >= 0) {
				_ipEnding = temp;
			}
			else {
				_ipEnding++;
			}
		}
		else {
			// _ipEnding <= myIpEnding
			_ipEnding = _myIpEnding + (_myIpEnding - _ipEnding); 
		}
		
		temp = _myIpEnding - _ipAddressRange / 2;
		
		if (temp < 0) {
			temp = Math.abs(temp);
		}
		else {
			temp = 0;
		}
		
		if (Math.abs(_ipEnding - _myIpEnding) > _ipAddressRange / 2 + temp) {
			// Start over
			_ipEnding = _firstIp;
		}
		
		final String retval = buffer.toString();
		System.out.println("DiscoveryClient::nextIp(): " + retval);
		log(retval);
		return retval;
	}
	
	/**
	 * Private internal class for timed broadcast sending.
	 */
    private class BroadcastTimerTask extends TimerTask {
        public void run() {
        	try {
    			_udpConnection =
    					(DatagramConnection)Connector.open("datagram://"
    							+ nextIp() + ":" + _port.toString());
        		send(_broadcastMessage);
        		_udpConnection.close();
        	}
        	catch (IOException e) {
    			log("DiscoveryClient::BroadcastTimerTask(): " + e.toString());
        	}
        	finally {
        		_udpConnection = null;
        	}
        	
        	_timeoutCounter++;
            
            if (_timeout != 0 && _timeoutCounter * BROADCAST_INTERVAL >= _timeout) {
            	log("Discovery timeout!");
            	notifyTimeout();
            	close();
            }
        }
    }
}
